﻿/*
 * Copyright (c) 2012 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

/**
Script to change the price and/or availability settings for your items in 
Merchant Center using the express update mechanism:
https://developers.google.com/commerce-search/docs/feeds#processing
Normal feed processing may take up to 24 hours but if you *just* change the
price or availability of an existing item, the updates go though to serving
much faster.  The trick is to update all the fields in an already serving items
exactly except for the price or availability settings.
This script accepts a list of itemID numbers and the new price/availability 
settings.  
Each item in the list is downloaded from Merchant Center usign the ContentAPI-batch-query
protocol (which ensures that all the fields you uploaded earlier is returned).
For each item, either the same data that was downloaded using the query is 
again uploaded with the new price or availability set.
REFERENCE : 
https://developers.google.com/shopping-content/getting-started/usingapi-products
http://code.google.com/p/gcs-admin-toolkit/wiki/GCSAuthentication
https://developers.google.com/accounts/docs/OAuth2#serviceaccount
http://code.google.com/p/gcs-admin-toolkit/
http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/auth/
USAGE:
    (first get an oauth2 private key and associate the oauth2 as an 
    administrator to your MC instance).  Specify your merchantID as the second param
    Acquire an oauth2 bearer token and call the update function.
    NOTE: oauth2 tokens expire after 1 hour
            Express e = new Express("1/1dD-ThRHIJP9UMKqxLkCdLZaZ7g6AAKqINoC3U0i4wU", "8437442");
            KeyValuePair<String, String> p = new KeyValuePair<string, string>("online:en:us:3", "11");
            KeyValuePair<String, String> pq = new KeyValuePair<string, string>("online:en:us:32312", "out of stock");
            List<KeyValuePair<String, String>> items_to_update = new List<KeyValuePair<string,string>>();
            items_to_update.Add(p);
            items_to_update.Add(pq);
            e.update(items_to_update);
 **/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Web;
using System.IO;
using System.Xml;
namespace GCSExpress
{
    class Express
    {
        String ACCESS_TOKEN = "";
        String cid = "";
        String[] VALID_AVAILABILITIES = {"in stock", "out of stock", "limited availability", 
                                             "available for order", "preorder"};
        static void Main(string[] args)
        {
            Express e = new Express("<YOUR AUTH TOKEN>", "<YOUR MERCHANT ID>");
            KeyValuePair<String, String> p = new KeyValuePair<string, string>("online:en:us:<ITEM1 ID>", "in stock");
            KeyValuePair<String, String> pq = new KeyValuePair<string, string>("online:en:us:<ITEM2 ID>", "11");
            List<KeyValuePair<String, String>> items_to_update = new List<KeyValuePair<string, string>>();
            items_to_update.Add(p);
            items_to_update.Add(pq);
            e.update(items_to_update);
        }
        public Express(String token, String cid)
        {
            this.ACCESS_TOKEN = token;
            this.cid = cid;
        }
        public void update(List<KeyValuePair<String, String>> items_to_update)
        {
            if (items_to_update.Count > 100)
            {
                // arbitrary 100 limit (batch feeds can process 1MB worth of data max...the number 100 is set out of nowhere)
                Console.WriteLine("Number of items to update per batch must be less than 100");
                Environment.Exit(1);
            }
            String BATCH_FEED = "https://content.googleapis.com/content/v1/" + this.cid + "/items/products/schema/batch";
            // access_token for oauth2
            // http://code.google.com/p/gcs-admin-toolkit/wiki/GCSAuthentication
            // https://developers.google.com/accounts/docs/OAuth2#serviceaccount
            // NOTE: oauth2 tokens are only valid for 1 hour.  Expired tokens should be renewed.
            // set SCOPE=https://www.googleapis.com/auth/structuredcontent
            // http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/auth/GCS_Auth.cs          
            BATCH_FEED = "https://content.googleapis.com/content/v1/" + this.cid + "/items/products/schema/batch?access_token=" + this.ACCESS_TOKEN;
            WebRequest request = WebRequest.Create(BATCH_FEED);
            // For client login
            // https://developers.google.com/accounts/docs/AuthForInstalledApps
            // http://code.google.com/p/gcs-admin-toolkit/source/browse/trunk/src/apiinsert/ClientLogin.java
            //request.Headers.Add("Authorization", "GoogleLogin auth=" + this.ACCESS_TOKEN);
            byte[] byteArray;
            String batch_template_start = "<feed xmlns='http://www.w3.org/2005/Atom' xmlns:batch='http://schemas.google.com/gdata/batch'>";
            String batch_template_end = "</feed>";
            String query_string = "";
            // For each item, create a batch query stanza
            foreach (KeyValuePair<String, String> item in items_to_update)
                query_string = query_string + ("<entry><batch:operation type='QUERY'/><batch:id>" + item.Key + "</batch:id><id>http://content.apis.google.com/content/v1/" + this.cid + "/items/products/schema/" + item.Key + "</id></entry>");
            query_string = batch_template_start + query_string + batch_template_end;
            //Console.WriteLine("Query Request: " + query_string);
            byteArray = Encoding.ASCII.GetBytes(query_string);
            // Now post the query as a batch request
            request.Method = "POST";
            request.ContentType = "application/atom+xml";
            request.Headers.Add("GDATA-Version", "1");
            request.ContentLength = byteArray.Length;
            Stream dataStream = request.GetRequestStream();
            dataStream.Write(byteArray, 0, byteArray.Length);
            dataStream.Close();
            WebResponse response = null;
            try
            {
                response = request.GetResponse();
                dataStream = response.GetResponseStream();
            }
            catch (WebException e)
            {
                string responseText;
                using (var rr = new StreamReader(e.Response.GetResponseStream()))
                {
                    responseText = rr.ReadToEnd();
                }
                Console.WriteLine("Query Error: " + e);
                Console.WriteLine(responseText);
                Environment.Exit(1);
            }
            StreamReader reader = new StreamReader(dataStream);
            string responseFromServer = reader.ReadToEnd();
            if (((HttpWebResponse)response).StatusCode == HttpStatusCode.OK)
            {
                // Console.WriteLine("Query Batch Processed");
            }
            else
            {
                Console.WriteLine("Query Batch not processed:  " + ((HttpWebResponse)response).StatusCode);
                Environment.Exit(1);
            }
            reader.Close();
            dataStream.Close();
            response.Close();
            //Console.WriteLine("Query Response: " + responseFromServer);
            //parse out the response, extract the data and remove the unnecessary nodes
            XmlDocument xdoc = new XmlDocument();
            xdoc.LoadXml(responseFromServer);
            //Console.WriteLine("Query Response: " + PrettyPrint(xdoc));
            //create a namespace context to work with
            XmlNamespaceManager nsMgr = new XmlNamespaceManager(xdoc.NameTable);
            nsMgr.AddNamespace("openSearch", "http://a9.com/-/spec/opensearchrss/1.0/");
            nsMgr.AddNamespace("scp", "http://schemas.google.com/structuredcontent/2009/products");
            nsMgr.AddNamespace("atom", "http://www.w3.org/2005/Atom");
            nsMgr.AddNamespace("batch", "http://schemas.google.com/gdata/batch");
            nsMgr.AddNamespace("sc", "http://schemas.google.com/structuredcontent/2009");
            nsMgr.AddNamespace("s", "http://www.google.com/shopping/api/schemas/2010");
            // make a node list of each offer in the response
            XmlNodeList entries = xdoc.SelectNodes("/atom:feed/atom:entry", nsMgr);
            // TODO: Error handling for feed error responses
            foreach (XmlNode entry in entries)
            {
                string id = HttpUtility.UrlDecode(entry["id"].InnerText);
                // extract out the batch status for this item..
                // if the batch status says 'not found' then the ID lookup failed...log it                        
                if (entry["batch:status"].Attributes.GetNamedItem("reason").Value.ToLower() == "item not found")
                {
                    Console.WriteLine("ITEM not found : " + id);
                    entry.ParentNode.RemoveChild(entry);
                }
                entry.RemoveChild(entry["batch:status"]);
                foreach (KeyValuePair<String, String> item in items_to_update)
                {
                    String canonical_id = "http://content.googleapis.com/content/v1/" + this.cid + "/items/products/schema/" + item.Key;
                    if (canonical_id.ToLower() == id.ToLower())
                    {
                        String pid = entry["sc:id"].InnerText;
                        //entry["id"].InnerText = pid;
                        bool availability_change = false;
                        foreach (String av in this.VALID_AVAILABILITIES)
                            if (av.ToLower() == (item.Value))
                                availability_change = true;
                        if (availability_change)
                            entry["scp:availability"].InnerText = item.Value;
                        else
                            entry["scp:price"].InnerText = item.Value;
                        //parse out the response, extract the data and remove the unnecessary nodes
                        entry.RemoveChild(entry["published"]);
                        entry.RemoveChild(entry["updated"]);
                        entry.RemoveChild(entry["app:edited"]);
                        entry.RemoveChild(entry["sc:expiration_date"]);
                        entry["batch:operation"].Attributes.GetNamedItem("type").Value = "update";

                        XmlNode appcontrol = entry["app:control"];
                        XmlNodeList scstatuses = appcontrol.SelectNodes("sc:status", nsMgr);
                        foreach (XmlNode status in scstatuses)
                        {
                            if (status.Attributes.GetNamedItem("dest").Value == "CommerceSearch" && status.Attributes.GetNamedItem("status").Value == "enlisted")
                            {
                                XmlElement req_dest = xdoc.CreateElement("sc:required_destination","http://schemas.google.com/structuredcontent/2009");
                                req_dest.SetAttribute("dest", "CommerceSearch");
                                appcontrol.AppendChild(req_dest);
                            }   
                            appcontrol.RemoveChild(status);
                        }
                        // uncomment to always use Google Commerce Search
                        //XmlElement req_dest = xdoc.CreateElement("sc:required_destination","http://schemas.google.com/structuredcontent/2009");
                        //req_dest.SetAttribute("dest", "CommerceSearch");
                        //appcontrol.AppendChild(req_dest);
                        
                        XmlNodeList links = entry.SelectNodes("atom:link", nsMgr);
                        foreach (XmlNode link in links)
                            if (link.Attributes.GetNamedItem("rel").Value != "alternate")
                                entry.RemoveChild(link);
                    }
                }
            }
            String update_request = PrettyPrint(xdoc);
            //Console.WriteLine("Update Request: " + update_request);
            byteArray = Encoding.ASCII.GetBytes(update_request);
            // Now post the update as a batch request
            request = WebRequest.Create(BATCH_FEED);
            request.Method = "POST";
            request.ContentType = "application/atom+xml";
            request.Headers.Add("GDATA-Version", "1");
            request.ContentLength = byteArray.Length;
            dataStream = request.GetRequestStream();
            dataStream.Write(byteArray, 0, byteArray.Length);
            dataStream.Close();
            response = null;
            try
            {
                response = request.GetResponse();
                dataStream = response.GetResponseStream();
            }
            catch (WebException e)
            {
                string responseText;
                using (var rr = new StreamReader(e.Response.GetResponseStream()))
                {
                    responseText = rr.ReadToEnd();
                }
                Console.WriteLine("Update Response Error: " + e);
                Console.WriteLine(responseText);
                Environment.Exit(1);
            }
            reader = new StreamReader(dataStream);
            responseFromServer = reader.ReadToEnd();
            //Console.WriteLine("Update Response: " + responseFromServer);
            // now parse through the update response and list out the status codes
            xdoc = new XmlDocument();
            xdoc.LoadXml(responseFromServer);
            //Console.WriteLine("Update Response: " + PrettyPrint(xdoc));
            entries = xdoc.SelectNodes("/atom:feed/atom:entry", nsMgr);
            foreach (XmlNode mentry in entries)
            {
                string mid = mentry["batch:id"].InnerText;
                Console.WriteLine("Update Status: itemID: " + mid + " ==> " + mentry["batch:status"].Attributes.GetNamedItem("reason").Value);
            }
            //Console.ReadLine();            
        }
        private String PrettyPrint(XmlDocument doc)
        {
            String resp = "";
            MemoryStream ms = new MemoryStream();
            XmlTextWriter writer = new XmlTextWriter(ms, Encoding.Unicode);
            try
            {
                writer.Formatting = Formatting.Indented;
                doc.WriteContentTo(writer);
                writer.Flush();
                ms.Flush();
                ms.Position = 0;
                StreamReader sr = new StreamReader(ms);
                String FormattedXML = sr.ReadToEnd();
                resp = FormattedXML;
            }
            catch (XmlException)
            {
                Console.WriteLine("XML Formatting Error");
            }
            ms.Close();
            writer.Close();
            return resp;
        }
    }
}
